/*
 * Copyright (c) 2010 Mathias Garbe <code@mathias-garbe.de>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include "Update.hpp"

namespace
{
    // Used by writeToMemoryCallback
    struct MemoryChunk {
        char *data;
        size_t size;
    };

    // This is the file write callback for curls download
    size_t writeToFileCallback(void *ptr, size_t size, size_t nmemb, FILE *stream) {
        size_t written;
        written = fwrite(ptr, size, nmemb, stream);
        return written;
    }

    // This is the memory write callback for curls download
    static size_t writeToMemoryCallback(void *ptr, size_t size, size_t nmemb, void *data)
    {
        size_t realsize = size * nmemb;
        struct MemoryChunk *mem = (struct MemoryChunk *)data;

        mem->data = (char*)realloc(mem->data, mem->size + realsize + 1);
        if (mem->data == NULL) {
            /* out of memory! */ 
            printf("not enough memory (realloc returned NULL)\n");
            exit(EXIT_FAILURE);
        }

        memcpy(&(mem->data[mem->size]), ptr, realsize);
        mem->size += realsize;
        mem->data[mem->size] = 0;

        return realsize;
    }

    // This is the download progress callback
    int progressCallback(void *ptr, double dtotal, double dnow, double ultotal, double ulnow)
    {
        double percent = (dnow * 100.0 / dtotal);

        Update* update = Update::instance();
        if(update == 0)
            throw std::runtime_error("Update instance got deleted but is still downloading.");

        if(update->win->close)
            return -1;

        Qt::QAppInstance()->processEvents(QEventLoop::AllEvents, 1);
        Qt::QAppInstance()->flush();

        update->win->setPercentage(percent);

        return 0;
    }
}

Update::Update()
{
    Update::instance(this);

    this->curl = curl_easy_init();
    if(!this->curl)
    {
        throw std::runtime_error("curl could not be initialized.");
    }
}

Update::~Update()
{   
    if(this->curl)
    {
        curl_easy_cleanup(this->curl);
    }

    Update::instance(0, true);
}

void Update::startupCheck(bool autoDownload)
{
    // Check if user is updating 
    std::string fullPath = Qt::QAppInstance()->applicationFilePath().toStdString();
    std::string exeName = fullPath.substr(fullPath.find_last_of("/")+1);

    if(exeName == "update.tmp")
    {
        QMessageBox::information(NULL, "CnCnet Update", "Applying update.");
        Sleep(1000);
        DeleteFileA(Qt::QAppInstance()->applicationDirPath().toStdString().append("/cncnet.old").c_str());
        MoveFileA(Qt::QAppInstance()->applicationDirPath().toStdString().append("/update.tmp").c_str(), 
                  Qt::QAppInstance()->applicationDirPath().toStdString().append("/cncnet.exe").c_str());

        QMessageBox::information(NULL, "CnCnet Update", "Update successfully applied.");
    }

    if(autoDownload)
    {
        this->doUpdate();
    }
}

bool Update::doUpdate()
{
    int build = this->getBuildVersion();

    if(build > _BUILD_VERSION)
    {
        if(this->downloadDependencies() && this->downloadUpdate())
        {
            DeleteFileA(Qt::QAppInstance()->applicationDirPath().toStdString().append("/cncnet.old").c_str());
            MoveFileA(Qt::QAppInstance()->applicationDirPath().toStdString().append("/cncnet.exe").c_str(), 
                      Qt::QAppInstance()->applicationDirPath().toStdString().append("/cncnet.old").c_str());

            // TODO: Check out if this is the way this is done.
            PROCESS_INFORMATION pInfo;
            STARTUPINFOA sInfo;
            memset(&sInfo, 0, sizeof(STARTUPINFO));
            CreateProcessA(NULL, (LPSTR)Qt::QAppInstance()->applicationDirPath().toStdString().append("/update.tmp").c_str(), 0, 0, FALSE, 0, 0, 0, &sInfo, &pInfo);
            exit(0);
        }
    }
    return true;
}

int Update::getBuildVersion()
{
    struct MemoryChunk chunk;

    chunk.data = (char*)malloc(1);
    chunk.size = 0;

    curl_easy_setopt(this->curl, CURLOPT_URL, "http://cncnet.cnc-comm.com/qtclient/version.txt");
    curl_easy_setopt(this->curl, CURLOPT_WRITEFUNCTION, writeToMemoryCallback);
    curl_easy_setopt(this->curl, CURLOPT_WRITEDATA, (void *)&chunk);
    curl_easy_setopt(this->curl, CURLOPT_USERAGENT, "CnCNet");
    curl_easy_setopt(this->curl, CURLOPT_TIMEOUT, 10);

    CURLcode code = curl_easy_perform(this->curl);

    // Well, some kind of error. I don't know. Don't need to.
    // Just return a negative version number.
    if(code != 0)
        return -1;

    this->version = atoi(chunk.data);

    if(chunk.data)
        free(chunk.data);

    return this->version; // DEBUG
}

bool Update::downloadDependencies()
{
    return true; // DEBUG


    struct MemoryChunk chunk;

    chunk.data = (char*)malloc(1);
    chunk.size = 0;

    char *url = "http://cncnet.cnc-comm.com/qtclient/dependencies/dependencies.txt";

    curl_easy_setopt(this->curl, CURLOPT_URL, url);
    curl_easy_setopt(this->curl, CURLOPT_WRITEFUNCTION, writeToMemoryCallback);
    curl_easy_setopt(this->curl, CURLOPT_WRITEDATA, (void *)&chunk);
    curl_easy_setopt(this->curl, CURLOPT_USERAGENT, "CnCNet");
    curl_easy_setopt(this->curl, CURLOPT_TIMEOUT, 10);

    CURLcode code = curl_easy_perform(this->curl);

    // Well, some kind of error. I don't know. Don't need to.
    // Just return a negative version number.
    if(code != 0)
        return false;

    std::vector<std::string> dependencies = std::split(chunk.data, '\n');

    if(chunk.data)
        free(chunk.data);
    
    this->win = new AutoUpdateWindow();
    this->win->show();
    this->win->setTitle("Downloading CnCnet Dependencies");

    for(int i=0; i<dependencies.size(); i++)
    {
        std::string sOut = Qt::QAppInstance()->applicationDirPath().toStdString().append("/").append(dependencies[i]);
        std::cout << sOut << std::endl;
        if(!FileExists(sOut.c_str()) && !this->win->close)
        {
            // Set window title to dll name
            std::stringstream title;
            title << "File (" << (i+1) << "/" << dependencies.size() << "): " << dependencies[i];
            this->win->setInfoLabel(title.str());

            // Set dll url
            std::string sUrl = "http://cncnet.cnc-comm.com/qtclient/dependencies/";
            sUrl.append(dependencies[i]);
            const char *url = sUrl.c_str();

            // Set dll file name and path
            char outfilename[4096];
            memset(outfilename, 0, 4096);
            if(sOut.size() >= 4095)
            {
                throw std::runtime_error("Buffer overflow in Update::downloadDependencies()");
            }
            strcpy_s(outfilename, 4096, sOut.c_str());
            //strcpy(outfilename, sOut.c_str()); // use strcpy_s?

            // Create directorys, if dll is in subdir
            std::vector<std::string> parts = std::split(dependencies[i], '/');
            std::string path = Qt::QAppInstance()->applicationDirPath().toStdString().append("/");
            for(int p = 0; p<parts.size()-1; p++)
            {
                path.append(parts[p]).append("/");
                CreateDirectoryA(path.c_str(), 0);
                std::cout << path << std::endl;
            }


            FILE* fp = fopen(outfilename, "w+b");

            curl_easy_setopt(this->curl, CURLOPT_URL, url);
            curl_easy_setopt(this->curl, CURLOPT_WRITEFUNCTION, writeToFileCallback);
            curl_easy_setopt(this->curl, CURLOPT_WRITEDATA, fp);
            curl_easy_setopt(this->curl, CURLOPT_NOPROGRESS, 0L);
            curl_easy_setopt(this->curl, CURLOPT_PROGRESSFUNCTION, progressCallback);
            curl_easy_setopt(this->curl, CURLOPT_PROGRESSDATA, 0);
            curl_easy_setopt(this->curl, CURLOPT_USERAGENT, "CnCNet");
            curl_easy_setopt(this->curl, CURLOPT_TIMEOUT, 0);

            CURLcode code = curl_easy_perform(this->curl);
            fclose(fp);

            if(code != 0)
            {
                DeleteFileA(Qt::QAppInstance()->applicationDirPath().toStdString().append(dependencies[i]).c_str());
                delete this->win;
                return false;
            }

        }
    }

    delete this->win;
    return true;
}

bool Update::downloadUpdate()
{
    this->win = new AutoUpdateWindow();
    this->win->show();
    this->win->setTitle("Downloading CnCnet Update");
    this->win->setInfoLabel("Downloading the latest client...");

    DeleteFileA(Qt::QAppInstance()->applicationDirPath().toStdString().append("/update.tmp").c_str());

    char url[4096];
    memset(url, 0, 4096);
    sprintf_s(url, 4096, "http://cncnet.cnc-comm.com/qtclient/updates/%d_cncnet.exe", this->version);

    char outfilename[4096];
    memset(outfilename, 0, 4096);
    if(Qt::QAppInstance()->applicationDirPath().toStdString().append("/update.tmp").size() >= 4095)
    {
        throw std::runtime_error("Buffer overflow in Update::downloadUpdate()");
    }
    strcpy_s(outfilename, 4096, Qt::QAppInstance()->applicationDirPath().toStdString().append("/update.tmp").c_str());
    //const char *outfilename = Qt::QAppInstance()->applicationDirPath().toStdString().append("/update.tmp").c_str();

    FILE* fp = fopen(outfilename, "w+b");

    curl_easy_setopt(this->curl, CURLOPT_URL, url);
    curl_easy_setopt(this->curl, CURLOPT_WRITEFUNCTION, writeToFileCallback);
    curl_easy_setopt(this->curl, CURLOPT_WRITEDATA, fp);
    curl_easy_setopt(this->curl, CURLOPT_NOPROGRESS, 0L);
    curl_easy_setopt(this->curl, CURLOPT_PROGRESSFUNCTION, progressCallback);
    curl_easy_setopt(this->curl, CURLOPT_PROGRESSDATA, 0);
    curl_easy_setopt(this->curl, CURLOPT_TIMEOUT, 0);

    CURLcode code = curl_easy_perform(this->curl);
    fclose(fp);

    if(code != 0)
    {
        delete this->win;
        return false;
    }

    delete this->win;
    return true;
}
